13. Synchronization
Synchronization
In this section, you will learn what synchronization is, when to use it, and some different ways to use in in Java.
ND079 JPND C2 L05 A13a Synchronization V2
What is Synchronization?
As you saw in a previous section, it's possible for multiple threads to access shared state, such as a List
or Map
stored in the heap. It's also possible multiple threads could be accessing a shared resource, such as a file.
In the context of multi-threaded programming, synchronization is the process of limiting the number of threads that can access a shared resource at the same time.
Synchronization is actually a more general concept that covers more than just threads. For example you can limit concurrent access of multiple processes, or programs, to a file. In this lesson we will only be talking about threads.
When is Synchronization Needed?
You should think about synchronization whenever you have multiple threads accessing the same shared resource, such as a data structure or a file.
If all the threads are just reading the shared resource, that's usually okay without synchronization. This is sometimes called read-only access to the shared resource.
On the other hand, if one or both of the threads is updating, or writing to, the shared resource, synchronization may be required!
SOLUTION:
- One thread reading from a file while the other writes.
- Two threads writing to the same `TreeMap`
Ways to Synchronize
ND079 JPND C2 L05 A13b Synchronization Continued
Java has several built-in utilities to help you synchronize multi-threaded code. Here are two examples:
- Synchronized collection wrappers:
Map<String, Integer> votes = Collections.synchronizedMap(new HashMap<>());
- Data structures and tools in the
java.util.concurrent
package that are specifically designed for concurrent access:
Map<String, Integer> votes = new ConcurrentHashMap<>();
SOLUTION:
`ConcurrentHashMap` is optimized to allow multi-threaded access without "blocking out" all but one thread.Synchronization Demo: Voting App
ND079 JPND C2 L05 A14 Demo Synchronization V2
Code from the Demo
Single-Threaded Version
import java.util.*;
import java.util.concurrent.*;
public final class VotingApp {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Map<String, Integer> votes = new HashMap<>();
List<Future<?>> futures = new ArrayList<>(10_000);
for (int i = 0; i < 10_000; i++) {
futures.add(
executor.submit(() -> {
votes.compute("Larry", (k, v) -> (v == null) ? 1 : v + 1);
}));
}
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
System.out.println(votes);
}
}
Multi-Threaded Version Using ConcurrentHashMap
import java.util.*;
import java.util.concurrent.*;
public final class VotingApp {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(12);
Map<String, Integer> votes = new ConcurrentHashMap<>();
List<Future<?>> futures = new ArrayList<>(10_000);
for (int i = 0; i < 10_000; i++) {
futures.add(
executor.submit(() -> {
votes.compute("Larry", (k, v) -> (v == null) ? 1 : v + 1);
}));
}
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
System.out.println(votes);
}
}
Multi-Threaded Version Using Collections.synchronizedMap()
import java.util.*;
import java.util.concurrent.*;
public final class VotingApp {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(12);
Map<String, Integer> votes = Collections.synchronizedMap(new HashMap<>());
List<Future<?>> futures = new ArrayList<>(10_000);
for (int i = 0; i < 10_000; i++) {
futures.add(
executor.submit(() -> {
votes.compute("Larry", (k, v) -> (v == null) ? 1 : v + 1);
}));
}
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
System.out.println(votes);
}
}
What are Atomic Operations?
An atomic operation is an operation that is executed as a single step, and cannot be split into smaller steps.
Atomicity is a really important concept in concurrent programming. If an operation is atomic, that means we don't have to worry about synchronizing it across different threads.
In the demo, you saw how ConcurrentHashMap.compute()
is an atomic operation, but individually calling ConcurrentHashMap.get()
and ConcurrentHashMap.put()
is not atomic.
SOLUTION:
- Checking the number of strings in the set.
- Checking if a string `"c"` is in the set.
- Checking if a string `"c"` is in the set and adding it if it's not already in the set.